﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.CodeDom.Compiler;
using System.IO;
using System.Globalization;

namespace BizElements.TemplateEngine
{
    /// <summary>Compiles and instantiates template expression objects.</summary>
    internal static class TemplateCompiler
    {
        /// <summary>Compiles an assembly that contains one class per each distinct expression found in given template.</summary>
        public static TemplateExpressionFactory CompileExpressions(Template template)
        {
            TemplateCompilation compilation = GenerateAssemblySourceCode(template);
            Assembly asm = CompileAssembly(template, compilation);
            TemplateExpressionFactory compiledExpressionFactory = new TemplateExpressionFactory(asm);
            return compiledExpressionFactory;
        }

        private sealed class TemplateCompilation
        {
            #region CTor.

            public TemplateCompilation()
            {
                this.Expressions = new List<ExpressionCompilationItem>();
            }

            #endregion

            #region Properties.

            public List<ExpressionCompilationItem> Expressions { get; set; }
            public string SourceCode { get; set; }
            public string OtherCompilationErrors { get; set; }

            #endregion

            #region Methods.

            public ExpressionCompilationItem GetItemByClassLineNumber(int classLineNumber)
            {
                foreach (ExpressionCompilationItem item in this.Expressions)
                {
                    bool lineBelongsToClass = (classLineNumber >= item.ClassFirstLineInSourceFile) && (classLineNumber <= item.ClassLastLineInSourceFile);
                    if (lineBelongsToClass)
                        return item;
                }

                return null;
            }

            public string GetCompilationErrorsDescription()
            {
                StringBuilder errmsg = new StringBuilder();
                if (!string.IsNullOrEmpty(this.OtherCompilationErrors))
                    errmsg.AppendLine(this.OtherCompilationErrors);

                foreach (ExpressionCompilationItem item in this.Expressions)
                {
                    if (!string.IsNullOrEmpty(item.CompilationError))
                        errmsg.AppendFormat(ItemErrorFormat, TextUtil.Trim(item.Expression), TextUtil.Trim(item.XmlLine), TextUtil.Trim(item.CompilationError));
                }

                return errmsg.ToString();
            }

            private static readonly string ItemErrorFormat = "Expression: {0}" + Environment.NewLine
                + "XML template line: {1}" + Environment.NewLine +
                "Error: {2}" + Environment.NewLine + Environment.NewLine;

            #endregion
        }

        private sealed class ExpressionCompilationItem
        {
            #region Properties.

            public string Expression { get; set; }
            //public string ClassName { private get; set; }
            //public string ClassSourceCode { private get; set; }
            public int ClassFirstLineInSourceFile { get; set; }
            public int ClassLastLineInSourceFile { get; set; }
            //public string ClassSourceFile { private get; set; }
            //public string XmlFile { private get; set; }
            //public int XmlLineNumber { private get; set; }
            //public int XmlColumnNumber { private get; set; }
            public string XmlLine { get; set; }
            public string CompilationError { get; set; }

            #endregion
        }

        #region Parse/extract expressions.

        private static List<string> GetAllDistinctExpressions(Template template)
        {
            List<string> allDistinctExpressions = new List<string>();
            if (template.InputComponentCriteria != null && !string.IsNullOrEmpty(template.InputComponentCriteria.BooleanExpression))
                allDistinctExpressions.Add(template.InputComponentCriteria.BooleanExpression);

            foreach (TemplateComponentVariableDefinition variable in template.ComponentVariableDefinitions)
            {
                if (!string.IsNullOrEmpty(variable.UsedForBooleanExpression))
                {
                    bool isNewExpression = !allDistinctExpressions.Contains(variable.UsedForBooleanExpression);
                    if (isNewExpression)
                        allDistinctExpressions.Add(variable.UsedForBooleanExpression);
                }
            }

            foreach (TemplateSegment segment in template.Segments)
            {
                var nonDistinctExpressions = GetAllExpressionsFromSegmentBody(segment.Body);
                foreach (string expression in nonDistinctExpressions)
                {
                    bool isNewExpression = !allDistinctExpressions.Contains(expression);
                    if (isNewExpression)
                        allDistinctExpressions.Add(expression);
                }
            }

            return allDistinctExpressions;
        }

        private static List<string> GetAllExpressionsFromSegmentBody(string segmentBody)
        {
            List<string> expressions = new List<string>();
            StringBuilder currExpressionSource = null;
            foreach (char token in segmentBody)
            {
                bool isExpressionStart, isBuildingExpression, isExpressionEnd;
                DetermineExpressionParserState(currExpressionSource, token, out isExpressionStart, out isBuildingExpression, out isExpressionEnd);
                if (isExpressionStart)
                {
                    currExpressionSource = new StringBuilder();
                }
                else if (isBuildingExpression)
                {
                    currExpressionSource.Append(token);
                }
                else if (isExpressionEnd)
                {
                    expressions.Add(currExpressionSource.ToString());
                    currExpressionSource = null;
                }
                // else skip token if not in expression.
            }

            return expressions;
        }

        private static void DetermineExpressionParserState(StringBuilder currExpressionSource, char token, out bool isExpressionStart, out bool isBuildingExpression, out bool isExpressionEnd)
        {
            bool isExpressionDelimiter = (token == Template.ExpressionDelimiter);
            isExpressionStart = false;
            isBuildingExpression = false;
            isExpressionEnd = false;
            if (isExpressionDelimiter)
            {
                if (currExpressionSource == null)
                    isExpressionStart = true;
                else
                    isExpressionEnd = true;
            }
            else
            {
                if (currExpressionSource != null)
                    isBuildingExpression = true;
            }
        }

        #endregion

        #region Generate ITemplateExpression classes - assembly source code with meta for easier debuggging.

        private static TemplateCompilation GenerateAssemblySourceCode(Template template)
        {
            // One class (ITemplateExpression implementation) per expression.
            List<string> allDistinctExpressions = GetAllDistinctExpressions(template);
            TemplateCompilation compilation = new TemplateCompilation();
            StringBuilder asmSource = new StringBuilder();

            GenerateUsingDirectives(template, asmSource);
            asmSource.AppendLine(NamespaceDeclaration);
            asmSource.AppendLine(" {");

            List<string> generatedClassNames = new List<string>();
            int firstLineOfCurrClass = TextUtil.CountLines(asmSource.ToString()) + 1;
            foreach (string expression in allDistinctExpressions)
            {
                string className = CreateUniqueClassName(expression);
                if (!generatedClassNames.Contains(className))
                {
                    string classSourceCode = GenerateExpressionClassSource(className, expression);
                    asmSource.AppendLine(classSourceCode);

                    int currClassLineCount = TextUtil.CountLines(classSourceCode);
                    ExpressionCompilationItem compilationItem = new ExpressionCompilationItem()
                    {
                        ClassFirstLineInSourceFile = firstLineOfCurrClass,
                        ClassLastLineInSourceFile = firstLineOfCurrClass + currClassLineCount,
                        //ClassName = className,
                        //ClassSourceCode = classSourceCode,
                        Expression = expression,
                        XmlLine = template.XmlLines.GetLineWithText(expression)
                    };
                    compilation.Expressions.Add(compilationItem);

                    // 1st line of next class.
                    firstLineOfCurrClass = compilationItem.ClassLastLineInSourceFile + 1;
                }
            }

            asmSource.AppendLine(" }");
            compilation.SourceCode = asmSource.ToString();
            return compilation;
        }

        private static void GenerateUsingDirectives(Template template, StringBuilder asmSource)
        {
            bool usesSystem = ArrayUtil.Exists<string>(template.Namespaces, (ns) => (ns == "System"));
            if (!usesSystem)
                asmSource.AppendLine("using System;");

            foreach (string name in template.Namespaces)
            {
                asmSource.AppendFormat("using {0};", name);
                asmSource.AppendLine();
            }

            if (template.Namespaces.Length > 0)
                asmSource.AppendLine();
        }

        public static string CreateUniqueClassName(string expression)
        {
            // String.GetHashCode() is not unique but it's a good trade of (uniqueness vs. performance).
            int hashCode = expression.GetHashCode();
            string uniqueName;
            if (hashCode < 0)
                uniqueName = string.Format(CultureInfo.InvariantCulture, "Tex_{0}", Math.Abs(hashCode));
            else
                uniqueName = string.Format(CultureInfo.InvariantCulture, "Tex{0}", hashCode);

            return uniqueName;
        }

        private static string GenerateExpressionClassSource(string className, string expression)
        {
            string classBody = ClassBodyFormat.Replace("###", expression);
            classBody = classBody.Replace("~~~", className);
            string classDeclarationAndBody = "  " + string.Format(CultureInfo.InvariantCulture, ClassDeclarationFormat, className) + Environment.NewLine + classBody;
            return classDeclarationAndBody;
        }

        const string NamespaceDeclaration = " namespace BizElements.TemporaryObjects";

        const string ClassDeclarationFormat = " public sealed class {0} : BizElements.TemplateEngine.TemplateExpressionBase";

        private static readonly string ClassBodyFormat =
            "   {" + Environment.NewLine +
            "      public override string Eval()" + Environment.NewLine +
            "      {     " + Environment.NewLine +
            "          return System.Convert.ToString(###);" + Environment.NewLine +
            "      }" + Environment.NewLine +
            "" + Environment.NewLine +
            "      public static BizElements.TemplateEngine.ITemplateExpression NewTemplateExpression()" + Environment.NewLine +
            "      {     " + Environment.NewLine +
            "          return new ~~~();" + Environment.NewLine +
            "      }" + Environment.NewLine +
            "   }" + Environment.NewLine;

        #endregion

        #region Compile and cache in RAM.

        private static Assembly CompileAssembly(Template template, TemplateCompilation compilation)
        {
            string source = compilation.SourceCode;
            Assembly cachedAssembly = TemplateExpressionAssemblyCache.GetAssembly(source);
            if (cachedAssembly != null)
                return cachedAssembly;

            CompilerParameters config = new CompilerParameters() { GenerateInMemory = true };
            foreach (string asmName in template.ReferencedAssemblies)
                config.ReferencedAssemblies.Add(asmName);

            if (!config.ReferencedAssemblies.Contains("BizElements.TemplateEngine.dll"))
                config.ReferencedAssemblies.Add("BizElements.TemplateEngine.dll");

            if (!config.ReferencedAssemblies.Contains("BizElements.Core.dll"))
                config.ReferencedAssemblies.Add("BizElements.Core.dll");
            
            // All referenced assemblies must be in the same directory as entry assembly.
            string oldCwd = Directory.GetCurrentDirectory();
            Directory.SetCurrentDirectory(IoUtil.GetExecutableDirectory());

            // C# 3.5 is default compiler unless changed in template.
            CodeDomProvider compiler = CreateCompiler(template.CodeDomProvider ?? CodeDomProviderInfo.DefaultCompiler);
            CompilerResults output = compiler.CompileAssemblyFromSource(config, source);
            Directory.SetCurrentDirectory(oldCwd);
            if (output.Errors.HasErrors)
            {
                SetCompilerErrors(compilation, output.Errors);
                string errmsg = compilation.GetCompilationErrorsDescription();
                throw new Exception(template.Header.Title + Environment.NewLine + errmsg);
            }

            TemplateExpressionAssemblyCache.SetAssembly(source, output.CompiledAssembly);
            return output.CompiledAssembly;
        }

        private static CodeDomProvider CreateCompiler(CodeDomProviderInfo settings)
        {
            Dictionary<string, string> options = new Dictionary<string,string>();
            options["CompilerVersion"] = settings.CompilerVersion;
            CodeDomProvider compiler = (CodeDomProvider)ReflectionUtil.CreateInstance(settings.AssemblyString, settings.ClassName, new object[]{options});
            return compiler;
        }

        private static void SetCompilerErrors(TemplateCompilation compilation, CompilerErrorCollection errors)
        {
            StringBuilder otherErrors = new StringBuilder();
            foreach (CompilerError err in errors)
            {
                ExpressionCompilationItem faultyItem = compilation.GetItemByClassLineNumber(err.Line);
                if (faultyItem != null)
                    faultyItem.CompilationError = GetErrorText(err);
                else
                    otherErrors.AppendLine(GetErrorText(err));
            }

            compilation.OtherCompilationErrors = otherErrors.ToString();
        }

        private static string GetErrorText(CompilerError error)
        {
            return error.ErrorText;
        }

        #endregion
    }

    /// <summary>Creates instances of classes derived from TemplateExpressionBase using prepared delegates/initializers - faster then reflection.</summary>
    sealed class TemplateExpressionFactory
    {
        #region Fields.

        Dictionary<string, Delegates.Function<ITemplateExpression>> initializers = new Dictionary<string, Delegates.Function<ITemplateExpression>>();

        #endregion

        #region CTor.

        public TemplateExpressionFactory(Assembly compiledTemplate)
        {
            Type[] allTypes = compiledTemplate.GetTypes();
            foreach (Type t in allTypes)
            {
                if (t.IsSubclassOf(typeof(TemplateExpressionBase)))
                {
                    MethodInfo staticInitMethod = t.GetMethod("NewTemplateExpression");
                    var init = (Delegates.Function<ITemplateExpression>)Delegate.CreateDelegate(typeof(Delegates.Function<ITemplateExpression>), staticInitMethod);
                    this.initializers.Add(t.Name, init);
                }
            }
        }

        #endregion

        #region Methods.

        public ITemplateExpression NewTemplateExpression(string expressionSource)
        {
            string expressionClassName = TemplateCompiler.CreateUniqueClassName(expressionSource);
            var init = this.initializers[expressionClassName];
            return init();
        }

        #endregion
    }
}